/**
 * Replace the following with your values:
 *  telescope - The name of this class
 *  %componentDescription% - The description of the component
 *  %uuid%                 - uuid from idl file for this component
 *  %contractID%           - Unique id for this component
 *  %componentCatagory%    - Catagory this component belongs to
 *  %componentEntry%       - Entry name in the catagory for this component
 *  %classComponent%       - Component name from the idl (like nsISupports)
 */

// Constants for convience
if (typeof(Cc) == "undefined")
  var Cc = Components.classes;
if (typeof(Ci) == "undefined")
  var Ci = Components.interfaces;
if (typeof(Cu) == "undefined")
  var Cu = Components.utils;
if (typeof(Cr) == "undefined")
  var Cr = Components.results;

Cu.import("resource://gre/modules/XPCOMUtils.jsm");

DEBUG=true;
function dumpn(str)
{
  if (DEBUG)
    dump(str + "\n");
}
/**
 * telescope
 */
function telescope() {
			Cu.import("resource://telescope/modules/HTTPUtils.js"); 
			Cu.import("resource://telescope/modules/JSON.js");
			Cu.import("resource://telescope/modules/Preferences.js"); 
			Cu.import("resource://telescope/modules/webtoolkit.base64.js"); 

		   Cu.import("resource://app/jsmodules/sbProperties.jsm");
		   Cu.import("resource://app/jsmodules/sbLibraryUtils.jsm");
};

telescope.prototype = {  
  /**
   * XPCOM Setup
   */
  classDescription: "Provides a web service and UI frontend via http",
  classID:          Components.ID("{de9572fb-ada6-4498-8fca-bfbb9280964b}"),  // unique CID
  contractID:       "@awilco.net/telescope;1",             // "@mozilla.org/example;1

  /**
   * The xpcom factory for this component, if not included then the default
   * will be used:
   *  new className()).QueryInterface(iid)
  //_xpcom_factory : { },
 
  /**
   * The Catagorys for this component, this can be used to search for all
   * components in the same catagory. It also defines if this is an instance
   * or a service. These are optional so you can comment out if you don't need
   * it, this will default to an instance in that case.
   */
  _xpcom_categories: [{
    category: "app-startup",  // Catagory for this component
    entry: "telescope",        // Catagory entry name
    service: true                    // Set to true to make this a service
  }],
 
  // Class functions and members here
  _server: null,
  _uiDirectory: '/',
  _serviceDirectory: '/service/',
  _ioService: Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService),
  _hooks: { mainPage:[], mediaItem: [], mediaList: [], mediaView: [], nowPlayingPane: [] },
  _authForRead: false,
  _authForWrite: false,
  _password: '',
  _respawnTimer: '',
  observe: function(aSubject, aTopic, aData) 
  {
		switch(aTopic) 
		{
		  case "app-startup":
			//listen for profile-after-change
            var obsService = Cc["@mozilla.org/observer-service;1"]
                           .getService(Ci.nsIObserverService);
            obsService.addObserver(this,'profile-after-change',false);
            break;
          case "profile-after-change":
			//place startup code here
			//Load preferences
		    this._authForRead = Preferences.get('extensions.telescope.authForRead');
		    this._authForWrite = Preferences.get('extensions.telescope.authForWrite');
		    this._password = Preferences.get('extensions.telescope.password');
			
			//Register service path handler
			this.server.registerPathHandler(this._serviceDirectory,this);
			
			//Register UI directory handle
			var ui_uri = this._ioService.newURI('resource://telescope/ui/',null,null).QueryInterface(Ci.nsIURL);
			this.server.registerURL(this._uiDirectory ,ui_uri, null);
			
			//Create a restart-server loop
			//TODO: fix this, sometimes it attemps to restart a running server...
			this._respawnTimer = Components.classes["@mozilla.org/timer;1"].createInstance(Components.interfaces.nsITimer);
			this._respawnTimer.initWithCallback({notify: this.callInContext(this.checkServerIsRunning)}, 5000, Components.interfaces.nsITimer.TYPE_REPEATING_SLACK);
			
			break;
	    }
  },

  
  /**
   * Gets the server for this instance
   **/
   get server()
   {
		if (this._server == null)
		{
			this._server = Cc['@awilco.net/jshttp;1'].createInstance(Components.interfaces.awIHttpServer);
			var port = Preferences.get('extensions.telescope.port');
            this._server.start(port);
		}
		return this._server;
	},
  /**
   * Handles internal service requests, use path as opposed to metadata.path
   * path is an array of path components
   */
    handle: function(metadata,response)
    {
		//First off, check for authorization
		if (this._authForRead) 
		{
			if (!this.checkAuthorization(metadata)) 
			{
				this.serveAuthorizationRequest(metadata,response);
				return;
			}
		}
		
		//decode path into components and params
		var path = metadata.path.substring(this._serviceDirectory.length).split('/');
		var params = HTTPUtils.decodeURIParameters(metadata.queryString);
		
		
		//Get first path component, and start with the main library as base media list
		var comp = path.shift().toLowerCase();
		var mediaList = LibraryUtils.mainLibrary;
		var data = {};
		
		switch(comp)
		{
			case 'library':
				if (path.length == 0 || path[0] == '')
					//Asking for information about the default library
					data = this.serveMediaListInfo(mediaList,params,metadata,response);
				else
				{	//Wanting to specify a library	
					var guid = path.shift();
                    mediaList = LibraryUtils.getMediaListByGUID(guid);
                    var view = mediaList.createView();
                    data = this.handleView('Library',view,path,params,metadata,response);
				}
				break;
			case 'player':
				if (metadata.method == 'POST')
					//Process player changes
					this.processPlayer(metadata,response);
				//Serve player status				
				return this.servePlayer(path,params,metadata,response);
            case 'hooks':
				//Serve list of hook urls
                data = this.serveHooks(path,metadata,response);
                break;
			default:
				//If we don't recognise it as a special command handle as a command for the library media list
				path.unshift(comp);
				data = this.handleMediaList(mediaList,path,params,metadata,response);
				break;
		}
		
		//write data
		if (typeof(data) != 'undefined')
        {
            response.setHeader("Content-Type", 'application/javascript', false);
			response.write(JSON.stringify(data));
        }
	},
	/**
		Sends the data back to client
		TODO: Handle other extensions?
	**/
	sendResponse: function(data,metadata,response)
	{
		//get the extension required, default to JSON
		var ext = this.getExtension(metadata.path);
		if (ext == '') ext = 'json';
		//TODO: handle differnt extensions
		response.setHeader("Content-Type", 'application/javascript', false);
		response.write(JSON.stringify(data));
		return;
	},
   /**
   * 
   */
    handleMediaList: function(mediaList,path,params,metadata,response)
    {
		var comp = '';
		if (path.length > 0)
			comp = path.shift().toLowerCase();
			
		var JSON = {};
		switch (comp)
		{
			case '':
				//Handles edits to a media list
				if (metadata.method == 'POST')
					this.processMediaList(mediaList,metadata);
				
				return this.serveMediaListInfo(mediaList,params,metadata,response);
			default:
				var view = mediaList.createView();
				path.unshift(comp);
				return this.handleView(mediaList.name,view,path,params,metadata,response);
		}
	},
   	/**
	* filters a media view by artist, album, title etc
	*/
	handleView: function(name,view,path,params,metadata,response)
	{
		var comp = '';
		var JSON = {};
		if (path.length > 0)
			comp = path.shift().toLowerCase();
			
		switch (comp)
		{
			case '':
				return this.serveMediaView(name,view,params,metadata,response)
			case 'playlist':
				//burrowing down into a sub media list
				//Doesn't actually have to be in the current view, can be any list
				var guid = path.shift();
				try
				{	mediaList = LibraryUtils.mainLibrary.getItemByGuid(guid);}
				catch(e)
				{	throw HTTPUtils.HTTP_404; }
				
				return this.handleMediaList(mediaList,path,params,metadata,response);
			//Filters for various properties
			case 'artist':
				var value = decodeURIComponent(path.shift());
				var propIndex = view.cascadeFilterSet.appendFilter(SBProperties.artistName);
				view.cascadeFilterSet.set(propIndex, [value], 1);
				return this.handleView(value,view,path,params,metadata,response);
			case 'album':
				var value = decodeURIComponent(path.shift());
				var propIndex = view.cascadeFilterSet.appendFilter(SBProperties.albumName);
				view.cascadeFilterSet.set(propIndex, [value], 1);
				return this.handleView(value,view,path,params,metadata,response);
			case 'genre':
				var value = decodeURIComponent(path.shift());
				var propIndex = view.cascadeFilterSet.appendFilter(SBProperties.genre);
				view.cascadeFilterSet.set(propIndex, [value], 1);
				return this.handleView(value,view,path,params,metadata,response);
			case 'year':
				var value = decodeURIComponent(path.shift());
				var propIndex = view.cascadeFilterSet.appendFilter(SBProperties.year);
				view.cascadeFilterSet.set(propIndex, [value], 1);
				return this.handleView(value,view,path,params,metadata,response);
			case 'track':
				var value = decodeURIComponent(path.shift());
				var propIndex = view.cascadeFilterSet.appendFilter(SBProperties.trackName);
				view.cascadeFilterSet.set(propIndex, [value], 1);
				return this.handleView(value,view,path,params,metadata,response);
			case 'playlists':
				//Serve playlists in this container
				return this.servePlaylists(view,params['searchStr'],params['fields'],params['limit']);
			case 'artists':
				return this.servePropertyBrowseView(SBProperties.artistName,view,params,metadata,response);
			case 'albums':
				return this.servePropertyBrowseView(SBProperties.albumName,view,params,metadata,response);
			case 'genres':
				return this.servePropertyBrowseView(SBProperties.genre,view,params,metadata,response);
			case 'tracks':
				return this.servePropertyBrowseView(SBProperties.trackName,view,params,metadata,response);
			case 'track':
				var guid = path.shift();
				try
				{	var mediaItem = LibraryUtils.mainLibrary.getItemByGuid(guid);}
				catch(e)
				{	throw HTTPUtils.HTTP_404; }
				return this.serveMediaItem(mediaItem,path,metadata,response);
				break;
			case 'search':
				return this.serveSearch(view,params,metadata,response);
			default:
				throw HTTPUtils.HTTP_401;
		}
	},	
	/**
	* parses a media view, with parameter filters
	**/
	serveMediaView: function(name,view,params,metadata,response)
	{	
		var JSON = {
            'guid': view.mediaList.guid,
			'name': view.mediaList.name,
			'listType': -1,
			'tracks': this.getMediaViewItems(view,params),
			};
		return this.sendResponse(JSON,metadata,response);
	},
    /**
   * parses a media list, with parameter filters
   */
    serveMediaListInfo: function(mediaList,params,metadata,response)
	{
		var info = {};
		info['guid'] = mediaList.guid;
		info['name'] = mediaList.name;
		info['listType'] = mediaList.type;
		info['tracks'] = this.getRequestedListItems(mediaList,params,metadata,response);
		return this.sendResponse(info,metadata,response);
	},
	getRequestedListItems: function(mediaList,params,metadata,response)
	{
		var view = LibraryUtils.createStandardMediaListView(mediaList);
		this.applySearch(view,params);
		return this.getMediaViewItems(view,params,metadata,response);
	},
	getMediaViewItems: function(view,params,metadata,response)
	{
		//parse limits
		this.applySearch(view,params);
		var limits = this.parseLimits(params);
		
		var count =0;
		var curMediaItem = null;
		var newListItem = null;
		var items = [];
		for (var i=0;i<view.length;i++)
		{
			if (i < limits.from)
				continue;
			if (i >= limits.to)
				break;
				
			curMediaItem = view.getItemByIndex(i);
			newListItem = this.getMediaItemInfo(curMediaItem);
				
			items.push(newListItem);
		}
		return items;
		
	},
	getMediaItemInfo: function (aMediaItem)
	{
		var info = {
					guid: aMediaItem.getProperty(SBProperties.GUID),
					name: aMediaItem.getProperty(SBProperties.trackName),
					artist: aMediaItem.getProperty(SBProperties.artistName),
					album: aMediaItem.getProperty(SBProperties.albumName),
					duration: aMediaItem.getProperty(SBProperties.duration),
					genre: aMediaItem.getProperty(SBProperties.genre),
					};
		//remove null values
		if (info.name == null) info.name = '';
		if (info.artist == null) info.artist = '';
		if (info.album == null) info.album = '';
		if (info.genre == null) info.genre = '';
		return info;
	},
  /**
   * parses a media list, with parameter filters
   */
	servePlaylists: function(view,searchStr,fields,limit)
	{
		var limit_from = -1; 
		var limit_to = Number.MAX_VALUE;
		if (limit)
		{
			var limits = limit.split(','); //TODO check if limit incorrectly formatted, if so bad request
			limit_from = parseInt(limits[0]);
			limit_to = limit_from + parseInt(limits[1]);
		}
		
		if (!fields) var fields = '';
		
		//TODO, handle searches
		//Change constraint to lists only
		view.filterConstraint = LibraryUtils.createConstraint([ [ [SBProperties.isList, ["1"]] ], [ [SBProperties.hidden, ["0"]] ] ]); 
		
		var count =0;
		var curMediaItem = null;
		var newListItem = null;
		var items = [];
		var listType = 1;
		for (var i=0;i<view.length;i++)
		{
			if (i < limit_from)
				continue;
			if (i >= limit_to)
				break;
				
			curMediaItem = view.getItemByIndex(i);
			//Some list items are device download thingymajiggys, ignore
			curMediaItem = curMediaItem.QueryInterface(Ci.sbIMediaList);
			if (!curMediaItem)
				continue;
			
			listType = curMediaItem.getProperty(SBProperties.listType);
			newListItem = {
					guid: curMediaItem.guid,
					name: curMediaItem.name,
					listLength: curMediaItem.length,
					listType: curMediaItem.type,
					};
			items.push(newListItem);
		}
		return items;		
	},
  /**
   * serves a list of items for a given media property
   */
	servePropertyBrowseView: function(property,view,params,metadata,response)
	{
		//TODO: Can we implement limits in a way that will save time?
		var values = [];
		var curItem = null;
		var exists = false;
		var limits = this.parseLimits(params);	
		this.applySearch(view,params);
		var filter = view.cascadeFilterSet;
			
		var valuesEnumerator = view.getDistinctValuesForProperty(property);
		var i=-1;
		while (valuesEnumerator.hasMore())
		{
			i++;
			if (i < limits.from)
				continue;
			if (i >= limits.to)
				break;
				
			values.push({ name: valuesEnumerator.getNext() });
		}
		return this.sendResponse(values,metadata,response);
	},

	/**
   * extracts the file extension from path info
   */
	getExtension: function(path)
	{
		var startIndex = path.indexOf('.');
		if (startIndex == -1)
			return '';
		
		var endIndex = path.indexOf('?');
		if (endIndex == -1)
			return path.substring(startIndex+1).toLowerCase();
		else
			return path.substring(startIndex+1,endIndex-startIndex-1).toLower();
	},
	/**
		parses the limits for a list from parameter list
	*/
	parseLimits: function(params)
	{
		var limits = { from: -1, to: Number.MAX_VALUE};
		if (params.limit)
		{
			var limits = params.limit.split(','); //TODO check if limit incorrectly formatted, if so bad request
			limits.from = parseInt(limits[0]);
			limits.to = limit_from + parseInt(limits[1]);
		}
		return limits;
	},
	/**
		applies search criteria to a view
	*/
	applySearch: function(view,params)
	{
		//handle old API
		if (params.searchStr) params.search = params.searchStr;
		if (!params.search) return;
		
		var filter = view.cascadeFilterSet;
		var index = filter.appendSearch(["*"],1);
		var searchArray = params.search.split(" ");
		filter.set(index, searchArray, searchArray.length);
	},
	/*
	 * Handles request to the player, including album art requests
	 */
	servePlayer: function(path,params,metadata, response)
	{
		var comp = '';
		if (path.length > 0)
			var comp = path.shift();
		
		var data = {};
		switch (comp)
		{
            case 'art':
                var gMM = Components.classes["@songbirdnest.com/Songbird/Mediacore/Manager;1"].getService(Components.interfaces.sbIMediacoreManager); 
                if (gMM.sequencer == null || gMM.sequencer.view == null || gMM.sequencer.viewPosition == -1)
                    throw HTTPUtils.HTTP_404;

                var curItem = gMM.sequencer.view.getItemByIndex(gMM.sequencer.viewPosition);
				return this.serveItemArt(curItem,metadata,response);
			case 'playlist':
				var gMM = Components.classes["@songbirdnest.com/Songbird/Mediacore/Manager;1"].getService(Components.interfaces.sbIMediacoreManager); 
				if (gMM.sequencer.view != null)
					data = this.getMediaViewItems(gMM.sequencer.view,params,metadata,response);
				else
					data = [];
				break;
			default:
				data = this.getPlayerInfo();
				break;
		}
		
		this.sendResponse(data,metadata,response);
	},
	/*
	 * returns information on the player
	 */
	getPlayerInfo: function()
	{
		var gMM = Components.classes["@songbirdnest.com/Songbird/Mediacore/Manager;1"].getService(Components.interfaces.sbIMediacoreManager); 
		
		var curItemIndex = -1;
		var curItem = null;
		var position = -1;
		var duration = -1;
		var volume = 0;
		var mode = 0;
		var repeatMode = 0;
		
		curItem = (gMM.sequencer.currentItem == null) ? null : this.getMediaItemInfo(gMM.sequencer.currentItem);
		if (gMM.sequencer != null)
		{
			repeatMode = gMM.sequencer.repeatMode;
			mode = gMM.sequencer.mode;
			
			if (gMM.sequencer.view != null)
			{
				try{
					curItemIndex = gMM.sequencer.viewPosition;
				}catch(e)
				{
					curItemIndex = -1;
					curItem = null;
				}
			}
		}
		if (gMM.playbackControl != null)
		{
			position = gMM.playbackControl.position;
			duration = gMM.playbackControl.duration;
		}
		if (gMM.volumeControl != null)
		{
			volume = gMM.volumeControl.volume;
			mute = gMM.volumeControl.mute;
		}
		var info = {
			'curItemIndex': curItemIndex,
			'curItem': curItem,
			'status': gMM.status.state,
			'position': position,
			'duration': duration,
			'mode': mode,
			'repeatMode': repeatMode,
			volume: volume,
			mute: mute,
			};
		
		return info;
	},
	/*
	 * sends the album art
	 */
	serveItemArt: function(item,metadata,response)
	{		
		var albumArtURL = item.getProperty(SBProperties.primaryImageURL);
		dump('Album art URL: ' + albumArtURL + "\n");
		if (albumArtURL == null)
			throw HTTPUtils.HTTP_404;
		
		var ios = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService);
		var uri = ios.newURI(albumArtURL,null,null);
		var url = uri.QueryInterface(Components.interfaces.nsIURL);

		dump('Album art extension: ' + url.fileExtension + "\n");
		var type =  Cc["@mozilla.org/uriloader/external-helper-app-service;1"]
				   .getService(Ci.nsIMIMEService)
				   .getTypeFromExtension(url.fileExtension);
		
		try {
			var channel = ios.newChannelFromURI(url);
			var is = channel.open();
			response.setHeader("Content-Type", type, false);
			response.bodyOutputStream.writeFrom(is, is.available());
			is.close();  
		}
		catch(e)
		{
			throw HTTPUtils.HTTP_404;
		}
		
	},
	/*
	 * processes requests on the player, such as play, pause, stop seek etc
	 */
	processPlayer: function(metadata,response)
	{
		//First off, check for authorization
		if (this._authForWrite)
		{
			if (!this.checkAuthorization(metadata))
			{
				this.serveAuthorizationRequest(metadata,response);
				return;
			}
		}
		
		//For now, cheat and get params from query string until we rewrite HTTPD.js
		var data = HTTPUtils.decodeURIParameters(metadata.postData);

		var gMM = Cc["@songbirdnest.com/Songbird/Mediacore/Manager;1"].getService(Components.interfaces.sbIMediacoreManager); 
		
		if (data['action'])
		{
			switch(data['action'])
			{
				case 'playMediaList':
					var mediaList = null;
					if (data['library'])
						mediaList = LibraryUtils.getMediaListByGUID(guid);
					else
						mediaList = LibraryUtils.mainLibrary;
						
					if (data['guid'])
						mediaList = mediaList.getMediaItem(data['guid']);
					
					var view = LibraryUtils.createStandardMediaListView(mediaList);
					
					if (view != null)
					{
						gMM.sequencer.playView(view,data['startIndex']);
					}
					break;
				case 'playCustomList':
					if (data['library'])
						mediaList = LibraryUtils.getMediaListByGUID(guid);
					else
						mediaList = LibraryUtils.mainLibrary;
					
					if (!data['guidList'])
					{
						dump('*** No Guids in custom list: ');
						return;
					}
					
					var guids = data['guidList'].split(',');
					var view = mediaList.createView();
					
					var filterIndex = view.cascadeFilterSet.appendFilter(SBProperties.GUID);
					view.cascadeFilterSet.set(filterIndex,guids,guids.length);
					
					
					gMM.sequencer.playView(view,data['startIndex']);
					break;
				case 'play':
					if (gMM.playbackControl != null)
						gMM.playbackControl.play();
                    else
                    {   //Turn on shuffle and play a random song
                        gMM.sequencer.mode = gMM.sequencer.MODE_SHUFFLE;
                        gMM.sequencer.playView(LibraryUtils.createStandardMediaListView(LibraryUtils.mainLibrary),gMM.sequencer.AUTO_PICK_INDEX);
                    }
					break;
				case 'pause':
					if (gMM.playbackControl != null)
						gMM.playbackControl.pause();
						
					this.server.stop();
					break;
				case 'next':
					if (gMM.sequencer != null)
						gMM.sequencer.next();
					break;
				case 'previous':
					if (gMM.sequencer != null)
						gMM.sequencer.previous();
					break;
				case 'mode':
					if (gMM.sequencer != null && data['mode'])
					{
								gMM.sequencer.mode = data['mode'];
					}
					break;
				case 'repeatMode':
					if (gMM.sequencer != null && data['mode'])
					{
						gMM.sequencer.repeatMode = data['mode'];
					}
					break;
				case 'seek':
					if (gMM.playbackControl != null && data['position'])
						gMM.playbackControl.position = parseInt(data['position']);
					break;
				case 'volume':
					if (gMM.volumeControl != null && data['volume'])
						gMM.volumeControl.volume = parseFloat(data['volume']);
					if (gMM.volumeControl != null && data['muted'])
						gMM.volumeControl.mute = (data['muted'] == 'false' ? false : true);
					
					break;
				case 'playIndex':
					if (gMM.playbackControl != null && gMM.sequencer != null && data['index'])
					{
						gMM.playbackControl.stop();
						gMM.sequencer.playView(gMM.sequencer.view,data['index']);
					}
					break;
					
			}
		}
	},
	/*
	 * performs a full search
	 */ 
	serveSearch: function(view,params,metadata,response)
	{
		var JSON = { artists: [], albums: [], tracks: [] };
		//If no query send a blank answer
		if (!params.search) return this.sendResponse(JSON,metadata,response);
		
		var queryRegex = new RegExp(params.search,'i');
		var name = '';
		this.applySearch(view,params);
		
		//artists
		var valuesEnumerator = view.getDistinctValuesForProperty(SBProperties.artistName);
		while (valuesEnumerator.hasMore())
		{
			var name = valuesEnumerator.getNext();
			if (name.search(queryRegex) > -1)
				JSON.artists.push({ name: name });
		}
		
		//albums
		var valuesEnumerator = view.getDistinctValuesForProperty(SBProperties.albumName);
		while (valuesEnumerator.hasMore())
		{
			var name = valuesEnumerator.getNext();
			if (name.search(queryRegex) > -1)
				JSON.albums.push({ name: name });
		}
		
		//Songs
		var newListItem = null;
		for (var i=0;i<view.length;i++)
		{
			curMediaItem = view.getItemByIndex(i);
			newListItem = this.getMediaItemInfo(curMediaItem);
			JSON.tracks.push(newListItem);
		}
		
		return this.sendResponse(JSON,metadata,response);
	},
	 /*
	 * serves a media Item
	 */
	serveMediaItem: function(item,path,metadata,response)
	{
		if (path.length == 0 || path[0] == '')
		{
		
			var ios = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService);
			// try
			// {
			var url = item.contentSrc.QueryInterface(Ci.nsIURL);
			// } catch (e) { throw HTTPUtils.HTTP_404; }
			var type =  Cc["@mozilla.org/uriloader/external-helper-app-service;1"]
					   .getService(Ci.nsIMIMEService)
					   .getTypeFromExtension(url.fileExtension);
			
			try 
			{
				var channel = ios.newChannelFromURI(item.contentSrc);
				var is = channel.open();
				response.setHeader("Content-Type", type, false);
				response.bodyOutputStream.writeFrom(is, is.available());
				is.close();  
			}
			catch(e)
			{
				throw HTTPUtils.HTTP_404;
			}
			return;
		}
		else
		{
			var comp = path.shift();
			switch (comp)
			{
				case 'info':
					return this.getMediaItemInfo(item);
                    break;
				case 'art':
                    this.serveItemArt(item,metadata,response);
                    return;
				case 'lyrics':
					return this.getItemLyrics(item,metadata,response);
			}
		}
	},
	processMediaList: function(metadata, mediaList)
	{
		//TODO: Implement media list editing
	},
	getItemLyrics: function(item,metadata,response)
	{
		var lyrics = item.getProperty('http://songbirdnest.com/data/1.0#lyrics');
		return lyrics;
	},
    serveHooks: function(path,metadata,response)
    {
        if (path.length < 1)
            throw HTTPUtils.HTTP_400;
        
        var comp = path.shift();
        switch (comp)
        {
            case 'mainPage':
            case 'mediaItem':
            case 'mediaList':
            case 'mediaView':
            case 'nowPlayingPane':
                var hooks = this._hooks[comp];
                return hooks;
        }
    },
    addHook: function(category,id,URL,label,imgURL)
    {
        if (!imgURL) var imgURL = null;
		dump('Adding hook: category: ' + category + ' id: ' + id + ' URL: ' + URL + ' label: ' + label + '\n');
        switch(category)
        {
            case 'mainPage':
            case 'mediaItem':
            case 'mediaList':
            case 'mediaView':
            case 'nowPlayingPane':
                this._hooks[category].push({ id:id, URL: URL, label: label, imgURL: imgURL });
                if (category == 'mediaView') //media view hooks also apply to media lists
                    this._hooks['mediaList'].push({ id:id, URL: URL, label: label, imgURL: imgURL });
                break;
            default:
                throw Cr.NS_ERROR_INVALID_ARG;
        }
		
    },
    removeHook: function(category,id)
    {
        switch(category)
        {
            case 'mainPage':
            case 'mediaItem':
            case 'mediaList':
            case 'mediaView':
            case 'nowPlayingPane':
                for (var i=0;i<this._hooks[category].length;i++) {if (this._hooks[category][i].id == id) this._hooks[category].splice(i,1); }
                if (category == 'mediaView')    //media view hooks also apply to media lists
					for (var i=0;i<this._hooks['mediaList'].length;i++) { if (this._hooks['mediaList'][i].id == id) this._hooks['mediaList'].splice(i,1); }
                break;
            default:
                throw Cr.NS_ERROR_INVALID_ARG;
        }
    },
    checkAuthorization: function(metadata)
	{
			
		try
		{
			var auth = metadata.getHeader("Authorization");
		}
		catch (e)
		{
			return false; 
		}
		var authorizationDecoded = Base64.decode(auth.replace(/Basic /, ""));
		var authorizationData = authorizationDecoded.split(":");
		
		if ((authorizationData[1] == this._password)) 
			return true;
		
		return false;
	},
	serveAuthorizationRequest: function(metadata,response)
	{
		response.setHeader("WWW-Authenticate", "Basic realm=\"Songbird Telescope\"", false);
		response.setStatusLine(metadata.httpVersion, 401, "Unauthorized");
		return;
	},
	get wrappedJSObject()
	{
		return this;
	},
	callInContext: function(fn) {
		var context = this;
		return function() { fn.apply(context, arguments); }
	},
	checkServerIsRunning: function()
	{
		//check we are still running
		try
		{
			var telescope = Components.classes["@awilco.net/telescope;1"].getService(Components.interfaces.nsISupports);
			telescope = telescope.wrappedJSObject;
		
				
			if (telescope.server && telescope.server.isStopped())
			{
				dump('>>> Telescope HTTP Server stopped, attempting restart...\n');
				var port = Preferences.get('extensions.telescope.port');
				telescope.server.start(port);
			}
		}
		catch(e)
		{
			dump(e);
		}
	},
  /**
   * Generates a query interface, include all components this class will
   * implement except nsISupports which is included by default.
   */
  QueryInterface: XPCOMUtils.generateQI([Ci.awITelescope, Ci.nsIObserver,  Ci.awIHttpRequestHandler, Ci.nsISupportsWeakReference])

  
  };

/**
 * NSGetModule is called to generate the module, pass all classes that need to
 * be implemented.
 */
function NSGetModule(compMgr, fileSpec) {
  return XPCOMUtils.generateModule([telescope]);
}